package com.compress4j.arithmetic;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * 
 * @author brunopierre4@yahoo.fr
 * 
 * Abstract class and implementation of EnCoder and Decoder
 *
 */
public abstract class Arithmetic {

	private final static int STARTING_BIT_COUNT = 31;
	
	private final static long STARTING_RANGE_LENGTH = 1l << STARTING_BIT_COUNT;

	long rangeStart = 0;
	long rangeLength = STARTING_RANGE_LENGTH;
	long rangeEnd = STARTING_RANGE_LENGTH;
	int rangeMaxBitCount = STARTING_BIT_COUNT;

	
	long valueReaded = 0;
	int notReadedBitCount = STARTING_BIT_COUNT;
	
	public void encode(int start, int end, int max) throws IOException {
		if(end==max && start==0){
			return;
		}
		// +1 ... -1 trick, allow to not have rounding issue when decoding (thanks coloquial)
		long intervalLengthPlusOne = (rangeLength+1);
		rangeEnd = rangeStart + (end * intervalLengthPlusOne) / (max) -1;
		rangeStart = rangeStart + (start * intervalLengthPlusOne) / (max);
		rangeLength = rangeEnd - rangeStart;

		//we must keep 2^31 <= rangeLength < 2^32 so it may be multiplied or divided by any positive integer
		while(rangeLength < STARTING_RANGE_LENGTH){
			rangeEnd <<=1;
			rangeStart <<=1;
			rangeLength <<=1;
			rangeMaxBitCount++;
			
			valueReaded <<=1;
			notReadedBitCount ++;
			//we cannot grow upper (Long.MAX_VALUE = 2^63-1) we must flush some data
			if(rangeMaxBitCount>=62){
				flushAByte();
			}
		}
	}
	/**
	 * flush a byte
	 * @throws IOException
	 */
	abstract void flushAByte() throws IOException;
	
	int commonFlush() {
		rangeMaxBitCount -= 8;
		long toflush = rangeStart>>rangeMaxBitCount;
		long startflush = toflush<<rangeMaxBitCount;
		valueReaded -= startflush;
		rangeStart -= startflush;
		rangeEnd = Math.min(rangeEnd - startflush, 1l<<rangeMaxBitCount);
		
		rangeLength = rangeEnd - rangeStart;
		return (int)toflush;
	}

	public static class Coder extends Arithmetic implements ArithmeticEncoder{

		private OutputStream out;
		/**
		 * Coder write to the stream byte by byte so please give it something Buffered 
		 * @param out
		 */
		public Coder(OutputStream out) {
			this.out = out;
		}
		
		void flushAByte() throws IOException {
			int toflush = commonFlush();
			out.write(toflush);
		}
		
		public void close() throws IOException {
			//written data must be upper than intervalStart, so we write the begin of the middle of the interval
			rangeStart = (rangeStart+rangeEnd)>>1;
			while(rangeMaxBitCount>STARTING_BIT_COUNT){
				flushAByte();
			}
			out.close();
		}
		
		public void flush() throws IOException {
			out.flush();
		}
	}
	
	public static class DeCoder extends Arithmetic implements ArithmeticDecoder{
		
		private InputStream in;
		/**
		 * micro buffer as long for the inputstream
		 */
		private long readedBuffered = 0;
		/**
		 * number of usable bit in the buffer
		 */
		private int readedBufferedBitCount = 0;
		
		/**
		 * DeCoder read the stream byte by byte so give him something Buffered 
		 * @param out
		 */
		public DeCoder(InputStream in) throws IOException{
			this.in = in;
			readInputStream();
		}
		void flushAByte() throws IOException {
			commonFlush();
		}
		
		public void hasDecoded(int start, int end, int max) throws IOException {
			encode(start, end, max);
			readInputStream();
		}
		public int read(int max) throws IOException {
			return (int) (((valueReaded-rangeStart + 1) * max - 1) / (rangeLength + 1));
		}

		public void close() throws IOException {
			in.close();
		}

		void readInputStream() throws IOException {
			//when reading at least 40 bits i will always have enought for fulling readedMantiseDispo which will be always <32
			while(readedBufferedBitCount<40){
				int r = in.read();
				if(r==-1){
					break;
				}
				readedBuffered = (readedBuffered<<8) + r;
				readedBufferedBitCount+=8;
			}
			int copyBitCount = Math.min(readedBufferedBitCount, notReadedBitCount);
			//removall of the data from buffer
			int moveOf = (readedBufferedBitCount-copyBitCount);
			long bitsToUse = (readedBuffered>>moveOf);
			readedBuffered -= (bitsToUse<<moveOf);
			readedBufferedBitCount -= copyBitCount;
			//add to readed
			notReadedBitCount -= copyBitCount;
			valueReaded += (bitsToUse<<(notReadedBitCount));
		}
	}
	
}
